FastAPI エラーハンドリング 戦略
情報収集・思考
例外設計に関する雑多な思考
まず大事なのは、APIが無いと仮定した上で、例外発生などを設計すること
参考:
API関数は必ず、システムの一番外側に位置することになる
これより内側の処理がここの層の知識を持ってはいけない
これをやってしまうとたちまち変更しにくいシステムになる
依存関係は一方向にしておくことが大事
例えば...「Item」と言うドメインモデルを表したクラスがあるとする
このItemが例外を発生させる場合、知ってる知識の範囲内で出せる例外を出す
このItemクラスを利用する処理のことを考慮した例外を発生させるとか絶対ダメ
外側の世界を知ってはいけない。たちまち密結合になる。
code: OK.py
class Item:
...
def remove_stock(self, count):
if self._stock < count:
raise ItemOutOfStockError()
self._stock -= 1
code: NG.py
class Item:
...
def remove_stock(self, count):
if self._stock < count:
raise APIBadRequestError()
self._stock -= 1
上のは極端な例ではあるが、こうならないためにも内側から例外設計していくと良き
とすると、API関数はどのように例外と付き合っていくのか
内側から様々な例外が飛んでくる。それらをどう捌いていくのか。
例外処理におけるAPI関数の責務を以下のように置く
ユーザーが適切なアクションを取れるよう、発生例外に合わせたHTTPステータスコード・メッセージをユーザーに返す
ユーザーが「どういうエラーが起きて自分が何をすれば回復するのか」がわかるようにしてあげる必要がある。
「フォームへの入力値が指定通りになっていない」などのエラーも「どの値がどう間違ってるのか」を伝えるようにする。
実装面で表現すると...基本的には以下を守れればいい
内側から上がってきた例外をAPI関数でキャッチして、適切なStatusCode, messageと共にエラーレスポンスを返すようにする
(API関数でキャッチしてそのまま回復処理するのもありそう)
さて、エラーレスポンスにはどんな情報があるといいんだろう?
StatusCode, messageだけだと、ユーザーは「?」となる
ここで情報収集
この記事一つで十分な気がする。すごい。
RFCで標準仕様が定義されてるっぽい。
けど、面倒臭そう...w
重要な点
1. 人間が読める、かつ、次のアクションが明確なエラーメッセージは絶対に含める
2. クライアントアプリがハンドリングできるよう、エラーコードを付与してあげる
3. UXを考えた際、複数エラーを持たせれるようにすることも考慮してもいいかも
4. APIの使い方を再認識してもらうために、APIのURLを付与してもいいかも
5. エラーレスポンスの構造には一貫性を持たせよう。絶対。
結論
code: error_response.json
{
"errors": [
{
"message": "エラーの詳細を記載する欄",
"type": "エラーコード"
},
...
]
}
4XXで返さないエラーは全て500で返そう
そして500にするエラーはAPI関数でキャッチしない。
キャッチしても意味ない。どうせ500なので。
400, 500に関しては、レスポンス時にしっかりエラー情報をロギングしておくことも大事。
容量が心配であれば、最悪400は記録しなくていいかも?
「500」は絶対に記録しろよ。絶対。フリじゃないぞ。
あと、リクエストパラメータ値などは必ずログとして記録しておく(LEVEL: INFO)
これがないと、500エラーが起きた時などのエラー調査ができなくなる。
エラーコードってどう言う感じがいいんだ?
参考:
数値で管理するのか、文字で管理するのか....
連番でもいいんじゃね?って言う人もいる
数字のエラーコードを使えば、ログの短縮化に繋がるんだとよ
文字の方がいいんじゃね?って言う人もいる
エラーコードを付与する目的を再考する
1. カスタマーサポート/ユーザーが、エラー原因を把握し解決アクションを取れるようにする
ユーザーがエラー原因に気づいて、正しいリクエストを行なってくれる
2. 開発者がエラー時にどの箇所で起きたものかを瞬時に把握できるようにする
エラーログを見た時に「このエラーはどの箇所のものだ?」が把握できるように。
pythonの場合、この問題はエラーコード無くても解決可能!
エラー発生ファイルは標準ライブラリのloggingで知ることができる。
3. エラーログの統計などに利用しやすくする
エラーログのエラー率(エラー毎の割合)などを統計できるように。
ありそうやけど、ん〜〜〜〜〜?wwww必要かねぇ?
4. クライアントアプリでエラーの分岐処理をしやすくする
SPAなどのアプリがエラーハンドリングをするために。
でもさ...
ステータスコードとエラーメッセージで分岐は十分説ない?
バリデーションエラーくらいやろ、細かい分岐が必要になりそうなの。
「1」と「4」が、目的としては妥当なところやなぁ
「1」のエラーコード粒度を小さくすれば、「4」の柔軟性も上がるな。
ん〜〜〜〜〜〜〜〜〜、悩みどころです。
エラーコード...
連番でやる場合はどんな名前にしよ
以下のルールはどう
ex)ESHP0-9999, ESHP1-9999
E: ERROR
BIK: サービスの接頭辞
0,1,..: システムカテゴリ
これはマイクロサービスというか、ドメイン別にAPIが存在する場合などに必要になる。
こっちのルールもいいのでは?
ex)shop.E9999
shop: サービスの接頭辞
E: エラー
9999: 識別番号
連番じゃない場合は?
名前をつけることになる
ex)shop.InvalidProductName, shop.InvalidProductStock的な
shop: サービスの接頭辞
Invalid...: エラー識別名
ん〜〜〜。連番の2つの目がいいかな?
そんな気がするな
内部の詳細をバラすようなエラーメッセージは出すな
って書かれてます。区別しろと。
内部のエラーメッセージとユーザー(外部向け)のエラーメッセージは分けろって。
FastAPIで例外戦略を実現するための情報収集・整理
参考情報
実現すべきことはいくつかある
① 内部処理で発生する例外のキャッチ
② エラーレスポンス用の例外を投げる
③ 要件に沿ったレスポンスボディの作成
④ 500系エラーの統一されたレスポンス
⑤ 全リクエストの入力情報のロギング
⑥ 全リクエストのエラー発生時のロギング
色々思考する
FastAPIの場合、API関数でエラーをキャッチせずに、エラーハンドリング関数でキャッチする方法もある
この方法だと、API関数のコードが簡略化されて読みやすくなるっちゃなる
逆に、API関数でキャッチ&エラーレスポンスの方法だと、API関数を実行するとどんなエラーが発生するのかが一目瞭然になる
どっちがいいのか
コードの可視化を取るのか、エラーレスポンスの可視化を取るのか
コードの可視化を一旦取ってみるか
エラー自体の設計はどうしよう
論点①:エラーコードはどうする
連番で行く(shop.E0001)
論点②:内部エラーメッセージと外部エラーメッセージは分けるのか
最初は分けないで行く。分ける必要性が出てきたら、その時方針を考えよう。